﻿//
// Connection pool code mostly taken from FluentCassandra. We thank them for their robust work!
//

using System;
using System.Collections.Generic;
using System.Threading;

namespace Sandy.Connections
{
    /// <summary>
    /// Connection pool object
    /// Pools are created by the PoolService and not directly.
    /// </summary>
    public class ConnectionPool
    {
        private readonly object _lock = new object();

        private readonly Queue<Connection> _freeConnections = new Queue<Connection>();
        private readonly List<Connection> _usedConnections = new List<Connection>();
        private readonly Timer _maintenanceTimer;
        
        private int _port;
        private string _host;

        /// <summary>
        /// Create a new pool.
        /// </summary>
        /// <param name="host">The host to connect to</param>
        /// <param name="port">The port to use with new connections</param>
        /// <param name="poolSize">Maximum number of connections to open</param>
        /// <param name="lifetime">Lifetime of idle connections. Connections at least this old (in ms) will be closed if idle.</param>
        internal ConnectionPool(string host, int port, int poolSize, int lifetime)
        {
            _host = host;
            _port = port;

            PoolSize = poolSize;
            Lifetime = lifetime;

            _maintenanceTimer = new Timer(o => Cleanup(), null, 30000L, 30000L);
        }

        /// <summary>
        /// Maximum number of connections
        /// </summary>
        private int PoolSize { get; set; }

        /// <summary>
        /// Lifetime of idle connections. Connections at least this old (in ms) will be closed if idle.
        /// </summary>
        private int Lifetime { get; set; }

        /// <summary>
        /// Create a new connection for this pool.
        /// Use SandyClient's (poolName, keyspace) c'tor and don't invoke this method directly.
        /// </summary>
        /// <returns>A new connection or an idle connection from the pool.</returns>
        internal Connection CreateConnection()
        {
            Connection conn = null;

            using (TimedLock.Lock(_lock))
            {
                if (_freeConnections.Count > 0)
                {
                    conn = _freeConnections.Dequeue();
                    _usedConnections.Add(conn);
                }
                else if (_freeConnections.Count + _usedConnections.Count >= PoolSize)
                {
                    if (!Monitor.Wait(_lock, TimeSpan.FromSeconds(30)))
                        throw new ConnectionException("No connection could be made, timed out trying to aquire a connection from the connection pool.");

                    return CreateConnection();
                }
                else
                {
                    conn = new Connection(_host, _port);
                    _usedConnections.Add(conn);
                }
            }

            return conn;
        }

        /// <summary>
        /// Close (mark as idle) an existing connection.
        /// SandyClient calls this method on Dispose. Don't invoke it directly.
        /// </summary>
        /// <param name="connection"></param>
        /// <returns></returns>
        internal bool Close(Connection connection)
        {
            using (TimedLock.Lock(_lock))
            {
                _usedConnections.Remove(connection);

                if (IsAlive(connection))
                    _freeConnections.Enqueue(connection);
            }

            return true;
        }

        /// <summary>
        /// Cleans up this instance.
        /// </summary>
        private void Cleanup()
        {
            CheckFreeConnectionsAlive();
        }

        /// <summary>
        /// Determines whether the connection is alive.
        /// </summary>
        /// <param name="connection">The connection.</param>
        /// <returns>True if alive; otherwise false.</returns>
        private bool IsAlive(Connection connection)
        {
            if (Lifetime > 0 && connection.Created.AddMilliseconds(Lifetime) < DateTime.Now)
                return false;

            return connection.IsOpen;
        }

        /// <summary>
        /// The check free connections alive.
        /// </summary>
        private void CheckFreeConnectionsAlive()
        {
            using (TimedLock.Lock(_lock))
            {
                var freeConnections = _freeConnections.ToArray();
                _freeConnections.Clear();

                foreach (var free in freeConnections)
                {
                    if (IsAlive(free))
                        _freeConnections.Enqueue(free);
                    else
                        free.Close();
                }
            }
        }
    }

    public class ConnectionException : Exception
    {
        public ConnectionException(string message) : base(message)
        {
            //
        }
    }
}
